/**
 * 
 */
package ro.dta.idbi.ui.sql;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.BorderFactory;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.table.DefaultTableModel;

import ro.dta.idbi.interfaces.ui.IMainFrame;
import ro.dta.idbi.interfaces.ui.ISchemaChooser;
import ro.dta.idbi.model.AbstractPattern;
import ro.dta.idbi.model.AutoMedUtils;
import ro.dta.idbi.model.QueryResult;
import uk.ac.bbk.dcs.automed.qproc.iql.ASG;
import uk.ac.bbk.dcs.automed.qproc.iql.Cell;
import uk.ac.ic.doc.automed.NotFoundException;
import uk.ac.ic.doc.automed.reps.Schema;
import uk.ac.ic.doc.automed.reps.SchemaObject;

/**
 * Functional Dependency tool dialog box
 * 
 * @author Tudor Dobrila
 * 
 */
public class FDTool extends JFrame implements ActionListener {

	private static final long serialVersionUID = 2530147658670882861L;

	/**
	 * Schema chooser, used to select a schema from the AutoMed repository
	 */
	private ISchemaChooser schemaChooser;

	/**
	 * Model used for the tables combo box
	 */
	private DefaultComboBoxModel<String> tablesModel;

	/**
	 * Model used for the result table
	 */
	private DefaultTableModel resultModel;

	/**
	 * No. of records in the selected table
	 */
	private int noRecords;

	/**
	 * Columns in the selected table
	 */
	private SchemaObject[] cols;

	/**
	 * Compute button, used to start the processing of FDs
	 */
	private JButton computeBtn;

	/**
	 * Functional dependency finder thread
	 */
	private Thread fdFinderThread;

	/**
	 * Functional dependency tool class constructor
	 * 
	 * @param parentFrame
	 *            Parent frame of the application
	 */
	public FDTool(IMainFrame parentFrame) {
		this(parentFrame, null, null);
	}

	/**
	 * Functional dependency tool class constructor
	 * 
	 * @param parentFrame
	 *            Parent frame of the application
	 * @param schema
	 *            Initial selected schema
	 * @param table
	 *            Initial selected table
	 */
	public FDTool(IMainFrame parentFrame, String schema, String table) {
		super();
		setSize(650, 500);
		setTitle("Functional dependency tool");
		setLocationRelativeTo(null);
		setIconImage(parentFrame.getIconImage());

		JPanel centerPanel = parentFrame.getGuiFactory().createGradientPanel(new GridBagLayout());
		getContentPane().add(centerPanel, BorderLayout.CENTER);

		GridBagConstraints constr = new GridBagConstraints();
		constr.fill = GridBagConstraints.BOTH;

		constr.gridwidth = 3;
		constr.gridx = 0;
		constr.gridy = 0;
		constr.weightx = 1.0;
		constr.weighty = 0.0;
		constr.ipady = 20;
		centerPanel.add(parentFrame.getGuiFactory().createTitleLabel("Functional Dependency Tool"),
				constr);

		constr.gridwidth = 1;
		constr.gridx = 0;
		constr.gridy = 1;
		constr.weightx = 0.0;
		constr.weighty = 0.0;
		constr.ipady = 0;
		constr.insets = new Insets(5, 5, 5, 5);
		centerPanel.add(new JLabel("Path to tool (FD.exe):"), constr);

		constr.gridx = 1;
		constr.gridy = 1;
		constr.weightx = 1.0;
		constr.weighty = 0.0;
		final JTextField pathField = new JTextField("resources/FD.exe");
		centerPanel.add(pathField, constr);

		constr.gridx = 2;
		constr.gridy = 1;
		constr.weightx = 0.0;
		constr.weighty = 0.0;
		JButton browseBtn = new JButton("Browse");
		browseBtn.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent arg0) {
				JFileChooser fileChooser = new JFileChooser(".");
				if (fileChooser.showOpenDialog(FDTool.this) == JFileChooser.APPROVE_OPTION) {
					pathField.setText(fileChooser.getSelectedFile().getAbsolutePath());
				}
			}
		});
		centerPanel.add(browseBtn, constr);

		constr.gridx = 0;
		constr.gridy = 2;
		constr.weightx = 0.0;
		constr.weighty = 0.0;
		centerPanel.add(new JLabel("Schema: "), constr);

		constr.gridwidth = 2;
		constr.gridx = 1;
		constr.gridy = 2;
		constr.weightx = 1.0;
		constr.weighty = 0.0;
		schemaChooser = parentFrame.getGuiFactory().createSchemaChooserPanel(schema);
		schemaChooser.getSchemasBox().addItemListener(new ItemListener() {

			@Override
			public void itemStateChanged(ItemEvent e) {
				if (schemaChooser.getSchemasBox().getSelectedIndex() != -1) {
					updateTablesBox(schemaChooser.getSelectedItem());
				}
			}
		});
		centerPanel.add((JPanel)schemaChooser, constr);

		constr.gridwidth = 1;
		constr.gridx = 0;
		constr.gridy = 3;
		constr.weightx = 0.0;
		constr.weighty = 0.0;
		centerPanel.add(new JLabel("Table: "), constr);

		constr.gridwidth = 2;
		constr.gridx = 1;
		constr.gridy = 3;
		constr.weightx = 0.0;
		constr.weighty = 0.0;
		tablesModel = new DefaultComboBoxModel<String>();
		updateTablesBox(schemaChooser.getSelectedItem());
		if (table != null) {
			tablesModel.setSelectedItem(table);
		}

		final JComboBox<String> tablesBox = new JComboBox<String>(tablesModel);
		centerPanel.add(tablesBox, constr);

		constr.fill = GridBagConstraints.NONE;
		constr.gridwidth = 3;
		constr.gridx = 0;
		constr.gridy = 4;
		constr.weightx = 1.0;
		constr.weighty = 0.0;
		computeBtn = new JButton("Compute functional dependencies");
		computeBtn.addActionListener(this);
		centerPanel.add(computeBtn, constr);

		constr.fill = GridBagConstraints.BOTH;
		constr.gridwidth = 3;
		constr.gridx = 0;
		constr.gridy = 5;
		constr.weightx = 1.0;
		constr.weighty = 1.0;
		resultModel = new DefaultTableModel();
		resultModel.setColumnCount(2);
		resultModel.setColumnIdentifiers(new String[] { "Determinant set", "Dependent set" });
		JTable resultsList = new JTable(resultModel);
		resultsList.setBorder(BorderFactory.createLineBorder(Color.BLACK));
		centerPanel.add(new JScrollPane(resultsList), constr);

		addWindowStateListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				super.windowClosing(e);

				if (fdFinderThread.isAlive()) {
					fdFinderThread.interrupt();
				}
			}
		});
	}

	/**
	 * Update the tables check box
	 * 
	 * @param schema
	 *            Schema for which to retrieve the tables
	 */
	private void updateTablesBox(String schema) {
		tablesModel.removeAllElements();

		try {
			Schema sch = Schema.getSchema(schema);
			SchemaObject[] schObjs = sch.getSchemaObjects();

			for (SchemaObject o : schObjs) {
				if (o.getConstruct().equals(AbstractPattern.table)) {
					tablesModel.addElement(o.getValue());
				}
			}
		} catch (NotFoundException e) {
			JOptionPane.showMessageDialog(this, "Could not find schema " + schema
					+ " in the AutoMed repository.", "Error", JOptionPane.ERROR_MESSAGE);
		}
	}

	/**
	 * Retrieve the information from the currently selected table and prepare
	 * the file with all records for TANE
	 * 
	 * @return True if the file has been successfully generated, false otherwise
	 */
	private boolean generateOriginalFile() {
		String selSchema = schemaChooser.getSelectedItem();
		Schema crtSchema = null;
		try {
			crtSchema = Schema.getSchema(selSchema);
		} catch (NotFoundException e) {
			JOptionPane.showMessageDialog(this, "Could not find schema " + selSchema
					+ " in the AutoMed repository.", "Error", JOptionPane.ERROR_MESSAGE);
			return false;
		}

		String selTbl = tablesModel.getSelectedItem().toString();
		SchemaObject tableObj = null;
		try {
			tableObj = crtSchema.getSchemaObject("<<" + selTbl + ">>");
		} catch (NotFoundException e) {
			JOptionPane.showMessageDialog(this, "Could not find table " + selTbl + " in schema "
					+ selSchema + ".", "Error", JOptionPane.ERROR_MESSAGE);
			return false;
		}

		// Get all columns belonging to the table
		cols = tableObj.getDependentSchemaObjects(AbstractPattern.column);

		// Query table to retrieve all rows
		StringBuilder builder = new StringBuilder("[ {");
		for (SchemaObject c : cols) {
			builder.append(c.getValue()).append(",");
		}
		builder.setLength(builder.length() - 1);
		builder.append("} | ");
		for (SchemaObject c : cols) {
			builder.append("{x,").append(c.getValue()).append("} <- <<").append(selTbl);
			builder.append(",").append(c.getValue()).append(">>; ");
		}
		builder.setLength(builder.length() - 2);
		builder.append(" ]");
		try {
			BufferedWriter writer = new BufferedWriter(new FileWriter("resources/original.tmp"));
			QueryResult result = AutoMedUtils.runIqlQuery(crtSchema, builder.toString());
			@SuppressWarnings("unchecked")
			final ArrayList<Cell> list = ASG.fromASGList(result.getAsg());

			noRecords = list.size();

			// Write all rows to the temporary file
			for (Cell n : list) {
				@SuppressWarnings("unchecked")
				final ArrayList<Cell> colsCell = ASG.fromASGTuple(n);
				for (int j = 0; j < colsCell.size(); j++) {
					Cell col = colsCell.get(j);
					writer.write(col.toString().replace(",", ""));
					if (j != colsCell.size() - 1) {
						writer.write(",");
					}
				}
				writer.write("\n");
			}
			writer.close();
		} catch (Exception e) {
			JOptionPane.showMessageDialog(this, "Could not query table " + selTbl + " in schema "
					+ selSchema + ".\r\nReason: " + e.getMessage(), "Error",
					JOptionPane.ERROR_MESSAGE);
			return false;
		}

		return true;
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		if (fdFinderThread == null || !fdFinderThread.isAlive()) {
			FDFinder finder = new FDFinder();
			fdFinderThread = new Thread(finder);
			fdFinderThread.start();
		}
	}

	/**
	 * Extract column indices from a string that contains a sequence of numbers,
	 * separated by spaces
	 * 
	 * @param str
	 *            String containing a sequence of numbers separated by spaces
	 * @return String containing the names of the columns with the specified
	 *         indices, separated by comma
	 */
	private String extractColumns(String str) {
		StringBuilder builder = new StringBuilder();
		Pattern numPatt = Pattern.compile("[0-9]+");
		Matcher matcher = numPatt.matcher(str);

		while (matcher.find()) {
			builder.append(cols[Integer.parseInt(matcher.group(0)) - 1].getValue());
			builder.append(", ");
		}
		builder.setLength(builder.length() - 2);

		return builder.toString();
	}

	/**
	 * Functional dependency finder thread, used to run TANE in a separate
	 * process and read the result returned by TANE
	 * 
	 * @author Tudor Dobrila
	 * 
	 */
	class FDFinder implements Runnable {

		@Override
		public void run() {
			computeBtn.setEnabled(false);

			if (!generateOriginalFile()) {
				computeBtn.setEnabled(true);
				return;
			}

			// Create description file for TANE
			try {
				BufferedWriter writer = new BufferedWriter(new FileWriter("resources/fddesc.tmp"));
				writer.write("Umask = 007\n");
				writer.write("DataIn = resources\\original.tmp\n");
				writer.write("#AttributesIn = \n");
				writer.write("RemoveDuplicates = OFF\n");
				writer.write("#AttributesOut = original.atr\n");
				writer.write("StandardOut = resources\\data.dat\n");
				writer.write("#SavnikFlachOut = resources\\data.rel\n");
				writer.write("NOOFDUPLICATES=1");
				writer.close();
			} catch (IOException e1) {
				JOptionPane.showMessageDialog(
						FDTool.this,
						"Could not generate description file for the tool.\r\nReason: "
								+ e1.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
				computeBtn.setEnabled(true);
				return;
			}

			// Convert to TANE format the file with original data
			String osName = System.getProperty("os.name");
			String selCmd = "resources\\fdselect.pl resources\\fddesc.tmp";
			if (osName.startsWith("Windows")) {
				selCmd = "cmd /C " + selCmd;
			}

			try {
				Process proc = Runtime.getRuntime().exec(selCmd);
				StringBuilder errorMsg = new StringBuilder();
				BufferedReader error = new BufferedReader(new InputStreamReader(
						proc.getErrorStream()));
				String line;
				while ((line = error.readLine()) != null) {
					errorMsg.append(line + "\n");
				}
				if (errorMsg.length() > 0) {
					JOptionPane
							.showMessageDialog(
									FDTool.this,
									"Could not execute the fdselect.pl script.\r\nReason: "
											+ errorMsg
											+ ". Please check that you have PERL installed on your computer.",
									"Error", JOptionPane.ERROR_MESSAGE);
					computeBtn.setEnabled(true);
					return;
				}

			} catch (IOException e1) {
				JOptionPane.showMessageDialog(FDTool.this,
						"Could not execute the select.pl script.\r\nReason: " + e1.getMessage(),
						"Error", JOptionPane.ERROR_MESSAGE);
				computeBtn.setEnabled(true);
				return;
			} finally {
				new File("resources/original.tmp").delete();
				new File("resources/fddesc.tmp").delete();
			}

			// Detect functional dependencies using TANE
			try {
				String fdName = "resources/FD";
				if (osName.startsWith("Windows")) {
					fdName += ".exe";
				}
				Process proc = Runtime.getRuntime().exec(
						fdName + " " + cols.length + " " + noRecords + " " + cols.length
								+ " resources/data.dat");
				BufferedReader input = new BufferedReader(new InputStreamReader(
						proc.getInputStream()));

				// Verify the no. of FDs
				String line;
				int crtRow = 0;
				Hashtable<String, StringBuilder> fds = new Hashtable<String, StringBuilder>();
				while ((line = input.readLine()) != null) {
					String[] lineTokens = line.split("->");
					if (!line.contains("(key)") && lineTokens.length == 2) {
						String detSet = extractColumns(lineTokens[0]);
						String depSet = extractColumns(lineTokens[1]);
						if (fds.containsKey(detSet)) {
							fds.get(detSet).append(", ").append(depSet);
						} else {
							fds.put(detSet, new StringBuilder(depSet));
							crtRow++;
						}
					}
				}

				if (crtRow == 0) {
					JOptionPane.showMessageDialog(FDTool.this,
							"No functional dependencies have been identified!", "Success",
							JOptionPane.INFORMATION_MESSAGE);
					computeBtn.setEnabled(true);
					return;
				}

				// Add the FDs to the results table
				resultModel.setNumRows(crtRow);
				crtRow = 0;
				for (Entry<String, StringBuilder> entry : fds.entrySet()) {
					resultModel.setValueAt("{ " + entry.getKey() + " }", crtRow, 0);
					resultModel.setValueAt("{ " + entry.getValue() + " }", crtRow, 1);
					crtRow++;
				}
			} catch (IOException e1) {
				JOptionPane.showMessageDialog(
						FDTool.this,
						"Could not run TANE functional dependency tool.\r\nReason: "
								+ e1.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
				computeBtn.setEnabled(true);
				return;
			} finally {
				new File("resources/data.dat").delete();
			}

			computeBtn.setEnabled(true);
		}
	}
}
